package filesystem.client;


import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;
import java.util.logging.Level;

import net.jxta.document.AdvertisementFactory;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.IDFactory;
import net.jxta.logging.Logging;
import net.jxta.peergroup.PeerGroup;
import net.jxta.pipe.PipeID;
import net.jxta.pipe.PipeService;
import net.jxta.platform.NetworkManager;
import net.jxta.protocol.PipeAdvertisement;
import net.jxta.socket.JxtaServerSocket;
import net.jxta.socket.JxtaSocket;
import filesystem.server.Server;
import filesystem.util.CloseSocket;
import filesystem.util.Messages;




/**
 * Classe que gerencia os clientes
 *
 */
public class Client{
	private static PipeID pipeNode = null;

	/**
	 * Cria um PipeAdvertisement do nó.
	 * 
	 * @param peergroup O grupo de peers.
	 * @return O próprio PipeAdvertisement.
	 */
	public static PipeAdvertisement createAdvertisement(PeerGroup peergroup){
		if(Client.pipeNode == null){
			Client.pipeNode = ( PipeID ) IDFactory.newPipeID(peergroup.getPeerGroupID());
			System.out.println(pipeNode);
		}
		PipeAdvertisement advertisement = (PipeAdvertisement)
				AdvertisementFactory.newAdvertisement(PipeAdvertisement.getAdvertisementType());
		// Seta algumas propriedades
		advertisement.setPipeID(pipeNode);
		advertisement.setType(PipeService.UnicastType);
		advertisement.setName("Node");
		return advertisement;
	}

	/**
	 * Cria um PipeAdvertisement por um identificador de pipe.
	 * 
	 * @param peergroup O grupo de peers.
	 * @param socketNode O identificador do socket.
	 * @return O próprio PipeAdvertisement.
	 */
	public static PipeAdvertisement createAdvertisement(PeerGroup peergroup, String socketNode){
		try {
			PipeID socketID = ( PipeID ) IDFactory.fromURI(new URI(socketNode));
			PipeAdvertisement advertisement = (PipeAdvertisement)
					AdvertisementFactory.newAdvertisement(PipeAdvertisement.getAdvertisementType());
			// Seta algumas propriedades
			advertisement.setPipeID(socketID);
			advertisement.setType(PipeService.UnicastType);
			advertisement.setName("Node");
			return advertisement;
		} catch (URISyntaxException e) {
			e.printStackTrace();
			System.exit(-1);
		}
		return null;
	}

	private transient NetworkManager networkManager;		// Client network
	private transient PeerGroup peerGroup;					// Client peergroup
	private JxtaSocket serverSocket;						// Socket to server
	private InputStreamReader dataInput;					// Input stream
	private DataOutputStream dataOutput;					// Output stream
	private BufferedReader bufferedReader;
	private PrintWriter printWriter;

	/**
	 * Obtém o PeerGroup do client.
	 * 
	 * @return O próprio PeerGroup
	 */
	public PeerGroup getPeerGroup(){
		return this.peerGroup;
	}

	/**
	 * Instancia a classe client e inicializa a rede do JXTA.
	 * 
	 * @param wait Se deve esperar Rendezvous.
	 */
	public Client(boolean wait) {
		Random rand = new Random();
		int id = rand.nextInt();
		try {
			networkManager = new NetworkManager(NetworkManager.ConfigMode.ADHOC, "SocketClient" + id,
					new File(new File(".cache"), "SocketClient" + id).toURI());
		} catch (IOException e) {
			System.out.println("ERROR: could not create client network");
			e.printStackTrace();
			System.exit(-1);
		}
		try {
			networkManager.startNetwork();
		} catch (PeerGroupException e) {
			System.out.println("ERROR: peer group error in client network");
			e.printStackTrace();
			System.exit(-1);
		} catch (IOException e) {
			System.out.println("ERROR: could not start client network");
			e.printStackTrace();
			System.exit(-1);
		}
		peerGroup = networkManager.getNetPeerGroup();
		if (wait) {
			networkManager.waitForRendezvousConnection(0);
		}		
	}

	/**
	 * Connecta o cliente ao servidor através do JxtaSocket.
	 * 
	 * Conecta o cliente ao servidor e obtém os streams para a comunicação.
	 */
	private void connect() {
		System.out.println("Connecting to server");
		try {
			serverSocket = new JxtaSocket(peerGroup, null, Server.createAdvertisement(this.peerGroup),60000,true);
		} catch (IOException e) {
			System.out.println("ERROR: could not connect to server");
			e.printStackTrace();
			System.exit(-1);
		}

		// Get output stream from socket
		try {
			dataOutput = new DataOutputStream(serverSocket.getOutputStream());
		} catch (IOException e) {
			System.out.println("ERROR: could not get output stream from socket");
		}		

		// Get input stream from socket
		try {
			dataInput = new InputStreamReader(serverSocket.getInputStream());
		} catch (IOException e) {
			System.out.println("ERROR: could not get input stream from socket");
		}

		printWriter = new PrintWriter(dataOutput,true);
		bufferedReader = new BufferedReader(dataInput);
	}
	
	/**
	 * Finaliza a conexão com o servidor.
	 */
	private void closeConnection(){
		try{
			this.printWriter.close();
			this.bufferedReader.close();
			this.dataInput.close();
			this.dataOutput.close();
			this.serverSocket.close();
		}catch(IOException e){
			e.printStackTrace();
		}
	}

	/**
	 * Envia mensagem ao servidor via socket.
	 * 
	 * Envia uma mensagem ao servidor e aguarda a resposta da mesma.
	 * 
	 * @param message String a ser enviada ao servidor.
	 * @return Resposta do servidor.
	 */
	public String sendMessage(String message){
		String ans = null;

		System.out.println("Sending: " + message);
		printWriter.println(message);
		printWriter.flush();
		try {
			System.out.print("Reading: ");
			ans = bufferedReader.readLine();
			System.out.println(ans);
		} catch (IOException e) {
			System.out.println("ERROR: could not read answer");
		}
		return ans;
	}

	/**
	 * Faz requisição de leitura de um arquivo.
	 * 
	 * @param path Caminho para o arquivo a ser enviado.
	 * @throws URISyntaxException
	 */
	public void Read(String path){
		this.connect();
		String response = this.sendMessage(Messages.read + " " + path + " " + Client.pipeNode);
		String content[] = response.split("[ ]+");

		// Received "ack"
		if(content[0].equals(Messages.ack)){
			if(content[1].equals(Messages.ok)){
				if(content[2].equals(Client.createAdvertisement(this.peerGroup))){
					System.out.println("File already update");
					this.printWriter.println(Messages.endRead + " " + Messages.ok);
					this.printWriter.flush();
				}else{
					JxtaSocket socket = null;
					PrintWriter writer = null;
					BufferedReader reader = null;
					try {
						socket = new JxtaSocket(this.peerGroup, Client.createAdvertisement(this.peerGroup,content[2]), 30000);
						writer = new PrintWriter(socket.getOutputStream());
						reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
						writer.println(Messages.get + " " + content[3]);
						writer.flush();
						response = reader.readLine();
						content = response.split("[ ]+");
						if(content[0].equals(Messages.ack) && content[1].equals(Messages.ok)){
							int current = 0;
							int byteRead = 0;
							System.out.println("Receiving  file...");
							byte [] buffer  = new byte [Integer.parseInt(content[2])];
							BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(path));
							InputStream inputStream = socket.getInputStream();
							while(current < buffer.length && byteRead != -1){
								byteRead = inputStream.read(buffer,current,buffer.length-current);
								current += byteRead;
							}
							outputStream.write(buffer);
							outputStream.flush();
							outputStream.close();
							writer.println(Messages.ok);
							writer.flush();
							System.out.println("Done");
							printWriter.println(Messages.endRead + " " + Messages.ok);
							this.printWriter.flush();
						}else if(content[0].equals(Messages.fail)){
							System.out.println("ERROR: denied by host client");
							printWriter.println(Messages.endRead + " " + Messages.fail);
							this.printWriter.flush();
						}else{
							System.out.println("Strange message");
							printWriter.println(Messages.endRead + " " + Messages.fail);
							this.printWriter.flush();
						}
					} catch (IOException e) {
						System.out.println("ERROR: could not connect to host client");
						e.printStackTrace();
						this.printWriter.println(Messages.endRead + " " + Messages.fail);
						this.printWriter.flush();
					}finally{
						if(writer != null){
							writer.close();
						}
						if(reader != null){
							try {
								reader.close();
							} catch (IOException e) {
								e.printStackTrace();
							}
						}
						if(socket != null){
							try {
								socket.close();
							} catch (IOException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}else if(content[1].equals(Messages.fail)){
				System.out.println("Fail to read: " + content[2]);
			}else{
				System.out.println("Strange message.");
			}
		}else{
			System.out.println("Strange message.");
		}
		this.closeConnection();
	}

	/**
	 * Faz requisição da remoção de um arquivo.
	 * 
	 * @param path Caminho do arquivo.
	 */
	public void Delete(String path){
		this.connect();
		String ret = this.sendMessage(Messages.delete + " " + path);
		if(ret != null){
			String pars[] = ret.split("[ ]+");
			if (pars[0].equals(Messages.ack)) {
				if(pars[1].equals(Messages.ok)){
					System.out.println("Delete " + path +" with success");
				}else if(pars[1].equals(Messages.fail)){
					System.out.println("Fail to delete " + path + " File Busy");
				}else{
					System.out.println("Strange Message");
				}
			}else{
				System.out.println("Strange Message");
			}
		}
		this.closeConnection();
	}

	/**
	 * Faz requisição da criacão de um arquivo.
	 * 
	 * @param path Caminho do arquivo.
	 */
	public void Create(String path){
		this.connect();
		String ret = this.sendMessage(Messages.create + " " + path + " " + Client.pipeNode);
		if(ret != null){
			String pars[] = ret.split("[ ]+");
			if (pars[0].equals(Messages.ack)) {
				if(pars[1].equals(Messages.ok)){
					System.out.println("Create " + path +" with success");
				}else if(pars[1].equals(Messages.fail)){
					System.out.println("Fail to create " + path + " " + " File already exists");
				}else{
					System.out.println("Strange Message");
				}
			}else{
				System.out.println("Strange Message");
			}
		}
		this.closeConnection();
	}
	
	/**
	 * Faz a requisição para a alteração de um arquivo
	 * 
	 * @param path O caminho da versão mais nova do arquivo.
	 */
	public void Write(String path){
		this.connect();
		String ret = this.sendMessage(Messages.write + " " + path + " " + Client.pipeNode);
		if(ret != null){
			String pars[] = ret.split("[ ]+");
			if (pars[0].equals(Messages.ack)) {
				if(pars[1].equals(Messages.ok)){
					System.out.println("Write " + path +" with success");
				}else if(pars[1].equals(Messages.fail)){
					if (pars[2].equals(Messages.notExists)) {
						System.out.println("Fail to write " + path + " " + " File not exists");
					} else if(pars[2].equals(Messages.busy)){
						System.out.println("Fail to write " + path + " " + " File Busy");
					}else{
						System.out.println("Strange Message");
					}
				}else{
					System.out.println("Strange Message");
				}
			}else{
				System.out.println("Strange Message");
			}
		}
		this.closeConnection();
	}

	/**
	 * Inicializa o cliente, aguardando conexões de outros nós e os comandos do usuário.
	 * 
	 * @param args Arquivo para armazenamento dos tempos das operações.
	 */
	public static void main(String[] args){
		System.setProperty(Logging.JXTA_LOGGING_PROPERTY, Level.OFF.toString());
		Client client = new Client(true);
		ClientOperator op;
		if(args.length > 0){
			op = new ClientOperator(client,args[0]);
		}else{
			op = new ClientOperator(client,"time " + new Random().nextInt() + ".log");
		}
		op.start();
		System.out.println("Starting node");
		JxtaServerSocket csock = null;
		try {
			csock = new JxtaServerSocket(client.getPeerGroup(), Client.createAdvertisement(client.getPeerGroup()),1000);
			Runtime.getRuntime().addShutdownHook(new CloseSocket(csock));
		} catch (IOException e) {
			System.out.println("ERROR: client failed to create a server socket");
			e.printStackTrace();
			System.exit(-1);
		}
		while(true){
			System.out.println("Waiting Connections");
			try {
				Socket socket = csock.accept();
				if (socket != null){
					System.out.println("New client connected");
					ClientConnection handler = new ClientConnection(socket);
					handler.start();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
